// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/ime/input_method_auralinux.h"

#include "base/auto_reset.h"
#include "base/environment.h"
#include "ui/base/ime/ime_bridge.h"
#include "ui/base/ime/ime_engine_handler_interface.h"
#include "ui/base/ime/linux/linux_input_method_context_factory.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/events/event.h"

namespace {

ui::IMEEngineHandlerInterface* GetEngine()
{
    if (ui::IMEBridge::Get())
        return ui::IMEBridge::Get()->GetCurrentEngineHandler();
    return nullptr;
}

} // namespace

namespace ui {

InputMethodAuraLinux::InputMethodAuraLinux(
    internal::InputMethodDelegate* delegate)
    : text_input_type_(TEXT_INPUT_TYPE_NONE)
    , is_sync_mode_(false)
    , composition_changed_(false)
    , suppress_next_result_(false)
    , weak_ptr_factory_(this)
{
    SetDelegate(delegate);
    context_ = LinuxInputMethodContextFactory::instance()->CreateInputMethodContext(
        this, false);
    context_simple_ = LinuxInputMethodContextFactory::instance()->CreateInputMethodContext(
        this, true);
}

InputMethodAuraLinux::~InputMethodAuraLinux()
{
}

LinuxInputMethodContext* InputMethodAuraLinux::GetContextForTesting(
    bool is_simple)
{
    return is_simple ? context_simple_.get() : context_.get();
}

// Overriden from InputMethod.

bool InputMethodAuraLinux::OnUntranslatedIMEMessage(
    const base::NativeEvent& event,
    NativeEventResult* result)
{
    return false;
}

void InputMethodAuraLinux::DispatchKeyEvent(ui::KeyEvent* event)
{
    DCHECK(event->type() == ET_KEY_PRESSED || event->type() == ET_KEY_RELEASED);

    // If no text input client, do nothing.
    if (!GetTextInputClient()) {
        ignore_result(DispatchKeyEventPostIME(event));
        return;
    }

    suppress_next_result_ = false;
    composition_changed_ = false;
    result_text_.clear();

    bool filtered = false;
    {
        base::AutoReset<bool> flipper(&is_sync_mode_, true);
        if (text_input_type_ != TEXT_INPUT_TYPE_NONE && text_input_type_ != TEXT_INPUT_TYPE_PASSWORD) {
            filtered = context_->DispatchKeyEvent(*event);
        } else {
            filtered = context_simple_->DispatchKeyEvent(*event);
        }
    }

    // If there's an active IME extension is listening to the key event, and the
    // current text input client is not password input client, the key event
    // should be dispatched to the extension engine in the two conditions:
    // 1) |filtered| == false: the ET_KEY_PRESSED event of non-character key,
    // or the ET_KEY_RELEASED event of all key.
    // 2) |filtered| == true && NeedInsertChar(): the ET_KEY_PRESSED event of
    // character key.
    if (text_input_type_ != TEXT_INPUT_TYPE_PASSWORD && GetEngine() && GetEngine()->IsInterestedInKeyEvent() && (!filtered || NeedInsertChar())) {
        ui::IMEEngineHandlerInterface::KeyEventDoneCallback callback = base::Bind(&InputMethodAuraLinux::ProcessKeyEventDone,
            weak_ptr_factory_.GetWeakPtr(),
            base::Owned(new ui::KeyEvent(*event)), filtered);
        GetEngine()->ProcessKeyEvent(*event, callback);
    } else {
        ProcessKeyEventDone(event, filtered, false);
    }
}

void InputMethodAuraLinux::ProcessKeyEventDone(ui::KeyEvent* event,
    bool filtered,
    bool is_handled)
{
    DCHECK(event);
    if (is_handled)
        return;

    // If the IME extension has not handled the key event, passes the keyevent
    // back to the previous processing flow. Preconditions for this situation:
    // 1) |filtered| == false
    // 2) |filtered| == true && NeedInsertChar()
    ui::EventDispatchDetails details;
    if (event->type() == ui::ET_KEY_PRESSED && filtered) {
        if (NeedInsertChar())
            details = DispatchKeyEventPostIME(event);
        else if (HasInputMethodResult())
            details = SendFakeProcessKeyEvent(event);
        if (details.dispatcher_destroyed)
            return;
        // If the KEYDOWN is stopped propagation (e.g. triggered an accelerator),
        // don't InsertChar/InsertText to the input field.
        if (event->stopped_propagation() || details.target_destroyed) {
            ResetContext();
            return;
        }

        // Don't send VKEY_PROCESSKEY event if there is no result text or
        // composition. This is to workaround the weird behavior of IBus with US
        // keyboard, which mutes the keydown and later fake a new keydown with IME
        // result in sync mode. In that case, user would expect only
        // keydown/keypress/keyup event without an initial 229 keydown event.
    }

    bool should_stop_propagation = false;
    // Note: |client| could be NULL because DispatchKeyEventPostIME could have
    // changed the text input client.
    TextInputClient* client = GetTextInputClient();
    // Processes the result text before composition for sync mode.
    if (client && !result_text_.empty()) {
        if (filtered && NeedInsertChar()) {
            for (const auto ch : result_text_) {
                ui::KeyEvent ch_event(*event);
                ch_event.set_character(ch);
                client->InsertChar(ch_event);
            }
        } else {
            // If |filtered| is false, that means the IME wants to commit some text
            // but still release the key to the application. For example, Korean IME
            // handles ENTER key to confirm its composition but still release it for
            // the default behavior (e.g. trigger search, etc.)
            // In such case, don't do InsertChar because a key should only trigger the
            // keydown event once.
            client->InsertText(result_text_);
        }
        should_stop_propagation = true;
    }

    if (client && composition_changed_ && !IsTextInputTypeNone()) {
        // If composition changed, does SetComposition if composition is not empty.
        // And ClearComposition if composition is empty.
        if (!composition_.text.empty())
            client->SetCompositionText(composition_);
        else if (result_text_.empty())
            client->ClearCompositionText();
        should_stop_propagation = true;
    }

    // Makes sure the cached composition is cleared after committing any text or
    // cleared composition.
    if (client && !client->HasCompositionText())
        composition_.Clear();

    if (!filtered) {
        details = DispatchKeyEventPostIME(event);
        if (details.dispatcher_destroyed) {
            if (should_stop_propagation)
                event->StopPropagation();
            return;
        }
        if (event->stopped_propagation() || details.target_destroyed) {
            ResetContext();
        } else if (event->type() == ui::ET_KEY_PRESSED) {
            // If a key event was not filtered by |context_| or |context_simple_|,
            // then it means the key event didn't generate any result text. For some
            // cases, the key event may still generate a valid character, eg. a
            // control-key event (ctrl-a, return, tab, etc.). We need to send the
            // character to the focused text input client by calling
            // TextInputClient::InsertChar().
            // Note: don't use |client| and use GetTextInputClient() here because
            // DispatchKeyEventPostIME may cause the current text input client change.
            base::char16 ch = event->GetCharacter();
            if (ch && GetTextInputClient())
                GetTextInputClient()->InsertChar(*event);
            should_stop_propagation = true;
        }
    }

    if (should_stop_propagation)
        event->StopPropagation();
}

void InputMethodAuraLinux::UpdateContextFocusState()
{
    bool old_text_input_type = text_input_type_;
    text_input_type_ = GetTextInputType();

    // We only focus in |context_| when the focus is in a textfield.
    if (old_text_input_type != TEXT_INPUT_TYPE_NONE && text_input_type_ == TEXT_INPUT_TYPE_NONE) {
        context_->Blur();
    } else if (old_text_input_type == TEXT_INPUT_TYPE_NONE && text_input_type_ != TEXT_INPUT_TYPE_NONE) {
        context_->Focus();
    }

    // |context_simple_| can be used in any textfield, including password box, and
    // even if the focused text input client's text input type is
    // ui::TEXT_INPUT_TYPE_NONE.
    if (GetTextInputClient())
        context_simple_->Focus();
    else
        context_simple_->Blur();

    ui::IMEEngineHandlerInterface* engine = GetEngine();
    if (engine) {
        ui::IMEEngineHandlerInterface::InputContext context(
            GetTextInputType(), GetTextInputMode(), GetTextInputFlags());

        if (old_text_input_type != TEXT_INPUT_TYPE_NONE)
            engine->FocusOut();
        if (text_input_type_ != TEXT_INPUT_TYPE_NONE)
            engine->FocusIn(context);

        ui::IMEBridge::Get()->SetCurrentInputContext(context);
    }
}

void InputMethodAuraLinux::OnTextInputTypeChanged(
    const TextInputClient* client)
{
    UpdateContextFocusState();
    InputMethodBase::OnTextInputTypeChanged(client);
    // TODO(yoichio): Support inputmode HTML attribute.
}

void InputMethodAuraLinux::OnCaretBoundsChanged(const TextInputClient* client)
{
    if (!IsTextInputClientFocused(client))
        return;
    NotifyTextInputCaretBoundsChanged(client);
    context_->SetCursorLocation(GetTextInputClient()->GetCaretBounds());

    if (!IsTextInputTypeNone() && text_input_type_ != TEXT_INPUT_TYPE_PASSWORD && GetEngine())
        GetEngine()->SetCompositionBounds(GetCompositionBounds(client));
}

void InputMethodAuraLinux::CancelComposition(const TextInputClient* client)
{
    if (!IsTextInputClientFocused(client))
        return;
    ResetContext();
}

void InputMethodAuraLinux::ResetContext()
{
    if (!GetTextInputClient())
        return;

    // To prevent any text from being committed when resetting the |context_|;
    is_sync_mode_ = true;
    suppress_next_result_ = true;

    context_->Reset();
    context_simple_->Reset();

    // Some input methods may not honour the reset call. Focusing out/in the
    // |context_| to make sure it gets reset correctly.
    if (text_input_type_ != TEXT_INPUT_TYPE_NONE) {
        context_->Blur();
        context_->Focus();
    }

    composition_.Clear();
    result_text_.clear();
    is_sync_mode_ = false;
    composition_changed_ = false;
}

void InputMethodAuraLinux::OnInputLocaleChanged()
{
}

std::string InputMethodAuraLinux::GetInputLocale()
{
    return "";
}

bool InputMethodAuraLinux::IsCandidatePopupOpen() const
{
    // There seems no way to detect candidate windows or any popups.
    return false;
}

// Overriden from ui::LinuxInputMethodContextDelegate

void InputMethodAuraLinux::OnCommit(const base::string16& text)
{
    if (suppress_next_result_ || !GetTextInputClient()) {
        suppress_next_result_ = false;
        return;
    }

    if (is_sync_mode_) {
        // Append the text to the buffer, because commit signal might be fired
        // multiple times when processing a key event.
        result_text_.append(text);
    } else if (!IsTextInputTypeNone()) {
        // If we are not handling key event, do not bother sending text result if
        // the focused text input client does not support text input.
        ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, 0);
        ui::EventDispatchDetails details = SendFakeProcessKeyEvent(&event);
        if (details.dispatcher_destroyed)
            return;
        if (!event.stopped_propagation() && !details.target_destroyed)
            GetTextInputClient()->InsertText(text);
        composition_.Clear();
    }
}

void InputMethodAuraLinux::OnPreeditChanged(
    const CompositionText& composition_text)
{
    if (suppress_next_result_ || IsTextInputTypeNone())
        return;

    if (is_sync_mode_) {
        if (!composition_.text.empty() || !composition_text.text.empty())
            composition_changed_ = true;
    } else {
        ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, 0);
        ui::EventDispatchDetails details = SendFakeProcessKeyEvent(&event);
        if (details.dispatcher_destroyed)
            return;
        if (!event.stopped_propagation() && !details.target_destroyed)
            GetTextInputClient()->SetCompositionText(composition_text);
    }

    composition_ = composition_text;
}

void InputMethodAuraLinux::OnPreeditEnd()
{
    if (suppress_next_result_ || IsTextInputTypeNone())
        return;

    if (is_sync_mode_) {
        if (!composition_.text.empty()) {
            composition_.Clear();
            composition_changed_ = true;
        }
    } else {
        TextInputClient* client = GetTextInputClient();
        if (client && client->HasCompositionText()) {
            ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, 0);
            ui::EventDispatchDetails details = SendFakeProcessKeyEvent(&event);
            if (details.dispatcher_destroyed)
                return;
            if (!event.stopped_propagation() && !details.target_destroyed)
                client->ClearCompositionText();
        }
        composition_.Clear();
    }
}

// Overridden from InputMethodBase.

void InputMethodAuraLinux::OnWillChangeFocusedClient(
    TextInputClient* focused_before,
    TextInputClient* focused)
{
    ConfirmCompositionText();
}

void InputMethodAuraLinux::OnDidChangeFocusedClient(
    TextInputClient* focused_before,
    TextInputClient* focused)
{
    UpdateContextFocusState();

    // Force to update caret bounds, in case the View thinks that the caret
    // bounds has not changed.
    if (text_input_type_ != TEXT_INPUT_TYPE_NONE)
        OnCaretBoundsChanged(GetTextInputClient());

    InputMethodBase::OnDidChangeFocusedClient(focused_before, focused);
}

// private

bool InputMethodAuraLinux::HasInputMethodResult()
{
    return !result_text_.empty() || composition_changed_;
}

bool InputMethodAuraLinux::NeedInsertChar() const
{
    return IsTextInputTypeNone() || (!composition_changed_ && composition_.text.empty() && result_text_.length() == 1);
}

ui::EventDispatchDetails InputMethodAuraLinux::SendFakeProcessKeyEvent(
    ui::KeyEvent* event) const
{
    KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, event->flags());
    ui::EventDispatchDetails details = DispatchKeyEventPostIME(&key_event);
    if (key_event.stopped_propagation())
        event->StopPropagation();
    return details;
}

void InputMethodAuraLinux::ConfirmCompositionText()
{
    TextInputClient* client = GetTextInputClient();
    if (client && client->HasCompositionText()) {
        client->ConfirmCompositionText();

        if (GetEngine())
            GetEngine()->Reset();
    }

    ResetContext();
}

} // namespace ui
